在我們能跟伺服器溝通後,就需要來定義伺服器跟客戶端如何發送跟接收對方所傳遞的資料。以 HTTP 協定來說就是一種溝通的方式,伺服器跟瀏覽器都已預先定義好的格式發送跟回傳資料,如此一來雙方就能有一個統一的方式解析跟處理傳輸的資料。
在遊戲中也是相同的,使用 TCP 連線的遊戲中大部分會選擇自己設計這個標準規格。而手遊則是比較多會考慮基於 HTTP 協定然後定義一些 HTTP API 來溝通,至於要選擇哪種方案通常會是受到遊戲的類型跟使用的語言所影響。
以 Unlight 為例子,我們在上一篇在閱讀 ULServer 的時候看到 #data2command
這個方法,他是在 EventMachine 接收到資料後呼叫 #receive_data
去處理封包後接著被執行,用途就是將 Unlight 規定好的格式轉換成「指令」然後再根據指令去呼叫對應的程式行為進行處理。
先來看一下 #data2command
這個方法做了些什麼。
def data2command(data)
a = []
i = 0
len =0
# 総サイズを記録
d_size = data.bytesize
# 総サイズ分読み込む
while i < d_size
# 最初の2バイトを呼んで長さに変換
len = data[i,2].unpack("n*")[0]
# もしサイズが0ならば全サイズが長さ,またはnilならば全サイズを入れる(とばすため)
len = d_size if len == 0||len ==nil
# 長さの後ろに改行が入っているか?(正しいコマンドかをチェック)
if data[i+len+2] == "\n"
d = @crypt.decrypt(data[i+2,len])
a << [d[0,2].unpack('n')[0], d[2..-1]]
end
i += (len+3)
end
a
end
我們從幾個關鍵的邏輯來看,首先是 data.bytesize
這個處理,在 Ruby 中 #bytesize
是用來計算位元組的長度,這表示 Unlight 可能是是自行設計的指令架構,如果是 HTTP 之類的應該直接解析字串即可。不過這還不足以判斷,繼續往下看到 len = data[i,2].unpack("n*")[0]
這行,Unlight 讀取了前面 2 Bytes 的資料來當作某個東西的長度。
在後面的 d = @crypt.decrypt(data[i+2,len])
就程式碼來看是依照前面解析出的的長度資訊,擷取固定的長度資料後後做解密(Decrypt)的處理,至少我們可以猜測 Unlight 有很高的機率是使用自行設計的指令,至少這些處理看起來不像是比較常見的協定。
因為是用 Ruby 實作的不容易看出資料的結構,我們用 C 語言的方式大概定義一下一個指令可能是長成這樣的。
typedef struct {
short int Length;
struct {
short int CommandID;
void* Content;
} Body;
} Command;
每個指令會有一個 2 Bytes 資料的頭(Header)作為長度的標記,接著會有一個指令內容。這個內容使用 2 Bytes 作為指令編號的定義,剩下的部分是一段長度不確定的資訊作為指令的內容(參數)。
名稱 | 型別 | 大小 |
---|---|---|
長度 | short int | 2 bytes |
指令 | short int | 2 bytes |
內容 | 指標 | 不固定 |
結尾 | char | 1 byte |
到這部分為止,我們大致上可以整理出像這樣的表格,要稍微注意的是上面的程式碼中 data[i+len+2] == "\n"
還呈現了一個資訊,就是每個指令必定以 \n
作為終止的標記。
順帶一提,在 #data2command
方法前面也有一段註解幫助其他人理解,畢竟要完全自己猜測是有點難度的。
# 受信コマンド
# データ形式は
# ---- Header ------
# :2byte(lentgh)
# :2byte(Type)
# ---- Body -------
# ---- Tail: -----
# :2byte(end_marker)
設計遊戲的指令系統的第一步基本上就是要先思考結構該長怎樣,不過其實不太需要過度複雜。指令系統除了可以跟伺服器溝通外,也很適合用來做遊戲內部的一些操作,像是 SLG 遊戲再確定行動時可以取消返回,就算是一種指令系統的應用。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。